<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title></title>
</head>
<body>
<script>
  const bucket = new WeakMap()
  let activeEffect = null
  let effectStack = []
  const effect = (fn, options = {}) => {
    const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      effectStack.push(effectFn)
      const res = fn()
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]
      return res
    }
    effectFn.deps = []
    effectFn.options = options
    if (!options.lazy) {
      effectFn()
    }
    return effectFn
  }

  function track(target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
      if (activeEffect !== effectFn) {
        effectsToRun.add(effectFn)
      }
    })

    effectsToRun && effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      effectFn.deps[i].delete(effectFn)
    }
    effectFn.deps.length = 0
  }

  const data = {
    foo: 1,
    bar: 2
  }

  const obj = new Proxy(data, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, newValue) {
      target[key] = newValue
      trigger(target, key)
      return true
    }
  })

  const computed = (getter) => {
    let value
    let dirty = true

    const effectFn = effect(getter, {
      lazy: true,
      scheduler() {
        dirty = true
        trigger(obj, value)
      }
    })

    const obj = {
      get value() {
        if (dirty) {
          value = effectFn()
          dirty = false
          track(obj, value)
        }
        return value
      }
    }

    return obj
  }


  const watch = (source, cb, options = {}) => {
    let getter
    if (typeof source === 'function') {
      getter = source
    } else {
      getter = () => traverse(source)
    }

    let oldVal, newVal
    const job = () => {
      newVal = effectFn()
      cb(newVal, oldVal)
      oldVal = newVal
    }
    const effectFn = effect(
      () => getter(),
      {
        lazy: true,
        scheduler() {
          if (options.flush === 'post') {
            const p = Promise.resolve()
            p.then(job)
          } else {
            job()
          }
      }
    })

    if (options.immediate) {
      job()
    } else {
      oldVal = effectFn()
    }

    function traverse(value, seen = new Set()) {
      if (typeof value !== 'object' || value === null || seen.has(value)) {
        return
      }
      seen.add(value)
      for (const key in value) {
        traverse(value[key], seen)
      }
      return value
    }
  }

  watch(() => obj.foo, (newVal, oldVal) => {
    console.log('读取obj.foo')
    console.log(newVal, oldVal)
  }, {
    immediate: true,
    flush: 'post',
  })

  obj.foo++
</script>
</body>
</html>
